<?php

/*
 * I hate writing parsers. But it always has to be done because I have
 * to do some interpreting of what the SQL code is doing.
 *
 * Recognizes:
 *
 * INSERT INTO <table> [colspec] VALUES <valspec>;
 * UPDATE <table> SET <col = val> WHERE <wherespc>;
 *
 * XXX/bowei -- reject multiple statements (handle ;) 
 *
 */

require_once('exceptions.php');

define('sql_regex_INSERT', '/^insert\s+into\s+/i');
define('sql_regex_UPDATE', '/^update\s+/i');
define('sql_regex_DELETE', '/^delete\s+from\s+/i');

define('sql_regex_BOOLEAN', '/^(true|false)/i');
define('sql_regex_NUMBER',  '/^-?[0-9]+(\.[0-9]+)?/');
define('sql_regex_FUNC',    '/^([a-zA-Z0-9_]+)\s*\(/');
define('sql_regex_ID',      '/^[a-zA-Z0-9_]+/');
define('sql_regex_OP',      '/^[-+*\/]/');
define('sql_regex_QUOTE',   '/^[\'"`]/');

//XXX/bowei --errors

class sql_Parser {
    function eat($re, $raise = true) {
        if ($re[0] != '/') {
            $re = '/^' . $re . '/';
        }
        if (preg_match($re, $this->str, $matched)) {
            $this->str = substr($this->str, strlen($matched[0]));
            $this->matched = $matched;
            return true;
        }

	if ($raise) {
	    throw new NeonError('Neon SQL parse error: looking for ' . $re .
                                ' but got "' . $this->str . '"');
	}

        return false;
    }

    function eat_ws($raise = false) {
        return $this->eat('\s+', $raise);
    }
 
    function parse_id_() {
        $quote_char = false;
        if ($this->eat('[\'"`]', false)) {
            $quote_char = $this->matched[0];
	}

        $this->eat('\w+');
        $id = $this->matched[0];

	if ($quote_char) {
	    $this->eat($quote_char);
        }

        return $id;
    }

    function parse_value_() {
        if ($this->eat(sql_regex_BOOLEAN, false) ||
            $this->eat(sql_regex_NUMBER, false)) {
            return $this->matched[0];
        } else {
            return $this->parse_str_();
        }
    }

    function parse_str_() {
        $quote_char = false;
    	if ($this->eat('[\'"`]')) {
            $quote_char = $this->matched[0];
        }
	$str = '';
        $slashed = false;

	# XXX/bowei -- this is slow, replace with preg_match
	while (strlen($this->str) > 0) {
            if (! $slashed) {
                if ($this->str[0] == $quote_char) {
                    break;
                }
                if (strlen($this->str[0]) == 0) {
                    throw new NeonError('Neon SQL parse error: runaway quoted string');
                }
                $slashed = ($this->str[0] == '\\');
            } else {
                $slashed = false;
            }
            $str .= $this->str[0];
	    $this->str = substr($this->str, 1);
        }

        if ($quote_char) {
            $this->eat($quote_char);
        }

        return $str;
    }

    function parse_wherespec_() {
        // XXX/bowei -- This is kind of bogus
        $this->eat('[^;]+');
        return trim($this->matched[0]);
    }

    function parse_insert_() {
        $tablename = $this->parse_id_();
        
        $this->eat_ws();

        // colspec
        $cols = array();
        if ($this->eat('\(', false)) {
            while (! $this->eat('\)', false)) {
                $this->eat_ws();
                $col = $this->parse_id_();
                if ($col) {
                    array_push($cols, $col);
                }
                $this->eat(',', false);
                $this->eat_ws();
            }
        }

        $this->eat_ws();
        $this->eat('/values/i');
        $this->eat_ws();

        // valspec 
        $vals = array();
        $this->eat('\(');
        while (! $this->eat('\)', false)) {
	    $this->eat_ws();
            //print("str: |$this->str|\n");
	    $val = $this->parse_value_();
            // XXX/bowei -- empty valspec is broken
            array_push($vals, $val);
	    $this->eat_ws();
	    $this->eat(',', false);
	    $this->eat_ws();
        }

        $this->eat(';');

        $r = new sql_Statement('insert');
        $r->table   = $tablename;
        $r->columns = $cols;
        $r->values  = $vals;
        
        return $r;
    }

    // Note: This grammar has the wrong associativity
    function parse_update_setspec_expr() {
        // const expr_rest
        if ($this->eat(sql_regex_BOOLEAN, false) ||
            $this->eat(sql_regex_NUMBER, false)) {
            $const = $this->matched[0];
            $node = new sql_updateSetExpr('const', $const);
            $this->eat_ws();

            $node = $this->parse_update_setspec_expr_rest($node);
            $this->eat_ws();

            return $node;
        } elseif(preg_match(sql_regex_QUOTE, $this->str)) {
            $const = $this->parse_str_();
            $node = new sql_updateSetExpr('const', $const);
            $this->eat_ws();

            $rest = $this->parse_update_setspec_expr_rest($node);
            $this->eat_ws();

            return $node;
        } elseif ($this->eat(sql_regex_FUNC, false)) {
            $func = $this->matched[1];
            $this->eat_ws();

            $args = array();
            
            if ($this->str[0] != ')') {
                while (true) {
                    $arg = $this->parse_update_setspec_expr();
                    array_push($args, $arg);
                    $this->eat_ws();
                    if (! $this->eat(',', false)) {
                        break;
                    }
                }
            }
            $this->eat_ws();
            $this->eat('\)');
            $this->eat_ws();

            $node = new sql_updateSetExpr('func', $func);
            $node->args = $args;
            $node = $this->parse_update_setspec_expr_rest($node);
            $this->eat_ws();

            return $node;
        } elseif ($this->eat(sql_regex_ID, false)) {
            $id = $this->matched[0];
            $node = new sql_updateSetExpr('id', $id);
            $this->eat_ws();

            $node = $this->parse_update_setspec_expr_rest($node);
            $this->eat_ws();

            return $node;
        } elseif ($this->eat('\(', false)) {
            $paren = $this->parse_update_setspec_expr();
            $this->eat('\)');
            $node = new sql_updateSetExpr('paren', $paren);
            $this->eat_ws();

            $node = $this->parse_update_setspec_expr_rest($node);
            $this->eat_ws();

            return $node;
        } else {
	    throw new NeonError("Neon SQL parse error: can't parse update_setspec, " . 
		                "looking at \"$this->str\"");
        }
    }

    function parse_update_setspec_expr_rest($node) {
        // , ; where will be epsilon
        if ($this->eat(sql_regex_OP, false)) {
            $op = $this->matched[0];
            $op_node = new sql_updateSetExpr('op', $op);
            $this->eat_ws();

            $op_node->left = $node;
            $op_node->right = $this->parse_update_setspec_expr();

            $this->eat_ws();
            return $op_node;
        } else {
            return $node;
        }
    }

    function parse_update_setspec_() {
        // id = val (, id = val)*
        $spec = array();
        
        while (true) {
            $id = $this->parse_id_();
            if ($id == 'where' or $id == 'WHERE') {
                //  XXX/bowei -- more clear parse errors
                throw new NeonError("Neon SQL parse error in update: unexpected " . 
                                    "WHERE clause in: \"$this->str\"");
            }
            
            $this->eat_ws();
            $this->eat('=');
            $this->eat_ws();
            $val = $this->parse_update_setspec_expr();
            $this->eat_ws();
            
            $spec[$id] = $val;
            
            if ($this->str[0] == ',') {
                $this->eat(',');
                $this->eat_ws();
            } else {
                break;
            }
        }
        return $spec;
    }

    function parse_update_() {
        $tablename = $this->parse_id_();

        $this->eat_ws();
        $this->eat('/set/i');
        $this->eat_ws();

        $valspec = $this->parse_update_setspec_();
        $this->eat_ws();

        if ($this->eat('/where/i', false)) {
            $wherespec = $this->parse_update_wherespec_();
        } else {
            $wherespec = array();
        }
        $this->eat(';');

        $sql = new sql_Statement('update');
        $sql->valspec = $valspec;
        $sql->wherespec = $wherespec;
        
        return $sql;
    }

    function parse_delete_() {
        $tablename = $this->parse_id_();
        
        $this->eat_ws();
        if ($this->eat('/where/i', false)) {
            $wherespec = $this->parse_wherespec_();
        } else {
            $wherespec = array();
        }
        $this->eat(';');

        $sql = new sql_Statement('delete');
        $sql->wherespec = $wherespec;
        
        return $sql;
    }

    function __construct() {
        $this->str = '';
        $this->matched = array();
    }

    function parse($str) {
        $this->str = $str;
        $this->matched = array();

        if ($this->eat(sql_regex_INSERT, false)) {
            return $this->parse_insert_();
        } elseif ($this->eat(sql_regex_UPDATE, false)) {
            return $this->parse_update_();
        } elseif ($this->eat(sql_regex_DELETE, false)) {
            return $this->parse_delete_();
        }
        return false;
    }
}

/*
 * Type defines the action of the statement.
 */
class sql_Statement {
    function __construct($type) {
        $this->type = $type;
    }
}

class sql_updateSetExpr {
    function __construct($type, $val) {
        $this->type = $type;
        $this->val  = $val;
    }
}

function sql_format_insert($table, $columns, $values) {
    $s = "INSERT INTO $table ";
    if (sizeof($columns) > 0) {
        $s .= '(' . implode($columns,', ') . ') ';
    }
    $s .= 'VALUES (';

    $first = true;
    foreach ($values as $v) {
        if (! $first) {
            $s .= ', ';
        }
        $s .= "'" . $v . "'";
        $first = false;
    }
    $s .= ');';

    return $s;
}

?>
